package com.mytoutou.playermanager.player_manager.ui;


import com.mytoutou.playermanager.player_manager.Config;
import com.mytoutou.playermanager.player_manager.utils.Logger;
import ohos.agp.components.Component;
import ohos.agp.graphics.Surface;
import ohos.agp.graphics.TextureHolder;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.global.resource.BaseFileDescriptor;
import ohos.global.resource.RawFileDescriptor;
import ohos.media.common.Source;
import ohos.media.player.Player;

import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * This class encapsulates {@link Player}
 * and follows this use-case diagram:
 */
public abstract class MediaPlayerWrapper implements Player.IPlayerCallback {

    //视频高度
    private int videoH = 0;
    //视频宽度
    private int videoW = 0;

    public void setVideoH(int videoH) {
        this.videoH = videoH;
    }

    public void setVideoW(int videoW) {
        this.videoW = videoW;
    }

    protected String TAG;
    private static final boolean SHOW_LOGS = Config.SHOW_LOGS;

    private double screenHeight = 800.0f;
    private double screenWidth = 1080.0f;

    public static final int POSITION_UPDATE_NOTIFYING_PERIOD = 1000;         // milliseconds
    private ScheduledFuture<?> mFuture;
    private Surface mSurface;


    public enum State {
        IDLE,
        INITIALIZED,
        PREPARING,
        PREPARED,
        STARTED,
        PAUSED,
        STOPPED,
        PLAYBACK_COMPLETED,
        END,
        ERROR
    }

    private final EventHandler mMainThreadHandler = new EventHandler(EventRunner.getMainEventRunner());
    private final Player mMediaPlayer;
    private final AtomicReference<State> mState = new AtomicReference<>();

    private MainThreadMediaPlayerListener mListener;
    private VideoStateListener mVideoStateListener;

    private ScheduledExecutorService mPositionUpdateNotifier = Executors.newScheduledThreadPool(1);

    public void rewindTo(long microseconds) {
        mMediaPlayer.rewindTo(microseconds);
    }

    protected MediaPlayerWrapper(Player mediaPlayer) {
        TAG = "" + this;
        if (SHOW_LOGS) Logger.v(TAG, "constructor of MediaPlayerWrapper");
        if (SHOW_LOGS)
            Logger.v(TAG, "constructor of MediaPlayerWrapper, main Looper " + EventRunner.getMainEventRunner());
        if (SHOW_LOGS) Logger.v(TAG, "constructor of MediaPlayerWrapper, my Looper " + EventRunner.current());

        if (EventRunner.current() != null) {
            throw new RuntimeException("myLooper not null, a bug in some MediaPlayer implementation cause that listeners are not called at all. Please use a thread without Looper");
        }
        mMediaPlayer = mediaPlayer;
        mState.set(State.IDLE);
        mMediaPlayer.setPlayerCallback(this);
    }

    private final Runnable mOnVideoPreparedMessage = new Runnable() {
        @Override
        public void run() {
            if (SHOW_LOGS) Logger.v(TAG, ">> run, onVideoPreparedMainThread");
            mListener.onVideoPreparedMainThread();
            if (SHOW_LOGS) Logger.v(TAG, "<< run, onVideoPreparedMainThread");
        }
    };

    public void setSurface(Surface surface) {
        mMediaPlayer.setVideoSurface(surface);
    }

    public void initParams() {
        mMediaPlayer.enableSingleLooping(false);
        mMediaPlayer.enableScreenOn(true);
    }

    public void prepare() {
        if (SHOW_LOGS) Logger.v(TAG, ">> prepare, mState " + mState);

        synchronized (mState) {
            switch (mState.get()) {
                case STOPPED:
                case INITIALIZED:
                    try {
                        //Logger.v(TAG, "prepare = " + mMediaPlayer.prepare());
                        mState.set(State.PREPARED);
                        if (mListener != null) {
                            Logger.v("MediaPlayerWrapper", "mMainThreadHandler");
                            mMainThreadHandler.postTask(mOnVideoPreparedMessage);
                        }

                    } catch (IllegalStateException ex) {
                        /** we should not call {@link MediaPlayerWrapper#prepare()} in wrong state so we fall here*/
                        throw new RuntimeException(ex);
                    }
                    break;
                case IDLE:
                case PREPARING:
                case PREPARED:
                case STARTED:
                case PAUSED:
                case PLAYBACK_COMPLETED:
                case END:
                case ERROR:
                    throw new IllegalStateException("prepare, called from illegal state " + mState);
            }
        }
        if (SHOW_LOGS) Logger.v(TAG, "<< prepare, mState " + mState);
    }

    /**
     * This method propagates error when {@link IOException} is thrown during synchronous {@link #prepare()}
     *
     * @param ex
     */
    private void onPrepareError(IOException ex) {
        if (SHOW_LOGS) Logger.err(TAG, "catch IO exception [" + ex + "]");
        // might happen because of lost internet connection
//      TODO: if (SHOW_LOGS) Logger.err(TAG, "catch exception, is Network Connected [" + Utils.isNetworkConnected());
        mState.set(State.ERROR);
        if (mListener != null) {
            mListener.onErrorMainThread(1, -1004); //TODO: remove magic numbers. Find a way to get actual error
        }
        if (mListener != null) {
            mMainThreadHandler.postTask(new Runnable() {
                @Override
                public void run() {
                    if (SHOW_LOGS) Logger.v(TAG, ">> run, onVideoPreparedMainThread");
                    mListener.onErrorMainThread(1, -1004); //TODO: remove magic numbers. Find a way to get actual error
                    if (SHOW_LOGS) Logger.v(TAG, "<< run, onVideoPreparedMainThread");
                }
            });
        }
    }

    /**
     * @see Player#setSource(Source) (Context, Uri)
     */
    public void setDataSource(String filePath) throws IOException {
        synchronized (mState) {
            if (SHOW_LOGS) Logger.v(TAG, "setDataSource, filePath " + filePath + ", mState " + mState);

            switch (mState.get()) {
                case IDLE:
                    mMediaPlayer.setSource(new Source(filePath));
                    mState.set(State.INITIALIZED);
                    break;
                case INITIALIZED:
                case PREPARING:
                case PREPARED:
                case STARTED:
                case PAUSED:
                case STOPPED:
                case PLAYBACK_COMPLETED:
                case END:


                case ERROR:
                default:
                    throw new IllegalStateException("setDataSource called in state " + mState);
            }
        }
    }

    /**
     * @see Player#setSource(BaseFileDescriptor) (FileDescriptor fd, long offset, long length)
     */
    public void setDataSource(RawFileDescriptor assetFileDescriptor) throws IOException {
        Logger.inf(TAG, "media =  " + mMediaPlayer);
        synchronized (mState) {
            switch (mState.get()) {
                case IDLE:
                    mMediaPlayer.setSource(assetFileDescriptor);
                    mState.set(State.INITIALIZED);
                    break;
                case INITIALIZED:
                case PREPARING:
                case PREPARED:
                case STARTED:
                case PAUSED:
                case STOPPED:
                case PLAYBACK_COMPLETED:
                case END:
                case ERROR:
                default:
                    throw new IllegalStateException("setDataSource called in state " + mState);
            }
        }
    }

    @Override
    public void onPrepared() {
        //doNothing
        Logger.inf(TAG, "onPrepared");
    }

    public void initPlayerViewSize(Component component) {
        int videoWidth = mMediaPlayer.getVideoWidth();
        int videoHeight = mMediaPlayer.getVideoHeight();
        Logger.inf(TAG, "VideoWidth = " + mMediaPlayer.getVideoWidth() + "Height =" + mMediaPlayer.getVideoHeight());
        if (videoWidth < videoHeight) {
            double scale = screenHeight * 1.f / videoHeight;
            double currHeight = this.videoH == 0 ? videoHeight * scale : this.videoH;
            double currWidth = this.videoW == 0 ? videoWidth * scale : this.videoW;
            component.setHeight(((int) currHeight));
            component.setWidth(((int) currWidth));
        } else {
            double scale = screenWidth * 1.f / videoWidth;
            double currHeight = this.videoH == 0 ? videoHeight * scale : this.videoH;
            double currWidth = this.videoW == 0 ? videoWidth * scale : this.videoW;
            component.setHeight(((int) currHeight));
            component.setWidth(((int) currWidth));
        }
    }

    @Override
    public void onResolutionChanged(int width, int height) {
        if (SHOW_LOGS) Logger.v(TAG, "onVideoSizeChanged, width " + width + ", height " + height);
      /*  if(!inUiThread()){
            throw new RuntimeException("this should be called in Main Thread");
        }*/
        if (mListener != null) {
            mListener.onVideoSizeChangedMainThread(width, height);
        }
    }

    @Override
    public void onPlayBackComplete() {
        if (SHOW_LOGS) Logger.v(TAG, "onVideoCompletion, mState " + mState);

        synchronized (mState) {
            mState.set(State.PLAYBACK_COMPLETED);
        }

        if (mListener != null) {
            mListener.onVideoCompletionMainThread();
        }
    }

    @Override
    public void onRewindToComplete() {
        // doNothing
    }

    @Override
    public void onError(int what, int extra) {
        if (SHOW_LOGS) Logger.v(TAG, "onErrorMainThread, what " + what + ", extra " + extra);

        synchronized (mState) {
            mState.set(State.ERROR);
        }

        if (positionUpdaterIsWorking()) {
            stopPositionUpdateNotifier();
        }
        if (SHOW_LOGS) Logger.v(TAG, "onErrorMainThread, mListener " + mListener);

        if (mListener != null) {
            mListener.onErrorMainThread(what, extra);
        }
        // We always return true, because after Error player stays in this state.
    }

    private boolean positionUpdaterIsWorking() {
        return mFuture != null;
    }

    @Override
    public void onBufferingChange(int percent) {
        Logger.inf(TAG, "onBufferingChange = " + percent);
        if (mListener != null) {
            mListener.onBufferingUpdateMainThread(percent);
        }
    }

    @Override
    public void onMessage(int what, int extra) {
        if (SHOW_LOGS) Logger.v(TAG, "onMessage");
        printInfo(what);
    }

    @Override
    public void onNewTimedMetaData(Player.MediaTimedMetaData var1) {

    }

    @Override
    public void onMediaTimeIncontinuity(Player.MediaTimeInfo var1) {

    }

    private void printInfo(int what) {
        switch (what) {
            case Player.PLAYER_INFO_UNKNOWN:
                if (SHOW_LOGS) Logger.inf(TAG, "onInfo, PLAYER_INFO_UNKNOWN");
                break;
            case Player.PLAYER_INFO_VIDEO_TRACK_LAGGING:
                if (SHOW_LOGS) Logger.inf(TAG, "onInfo, PLAYER_INFO_VIDEO_TRACK_LAGGING");
                break;
            case Player.PLAYER_INFO_VIDEO_RENDERING_START:
                if (SHOW_LOGS) Logger.inf(TAG, "onInfo, PLAYER_INFO_VIDEO_RENDERING_START");
                break;
            case Player.PLAYER_INFO_BUFFERING_START:
                if (SHOW_LOGS) Logger.inf(TAG, "onInfo, PLAYER_INFO_BUFFERING_START");
                break;
            case Player.PLAYER_INFO_BUFFERING_END:
                if (SHOW_LOGS) Logger.inf(TAG, "onInfo, PLAYER_INFO_BUFFERING_END");
                break;
            case Player.PLAYER_INFO_BAD_INTERLEAVING:
                if (SHOW_LOGS) Logger.inf(TAG, "onInfo, PLAYER_INFO_BAD_INTERLEAVING");
                break;
            case Player.PLAYER_INFO_NOT_SEEKABLE:
                if (SHOW_LOGS) Logger.inf(TAG, "onInfo, PLAYER_INFO_NOT_SEEKABLE");
                break;
            case Player.PLAYER_INFO_METADATA_UPDATE:
                if (SHOW_LOGS) Logger.inf(TAG, "onInfo, PLAYER_INFO_METADATA_UPDATE");
                break;
            case Player.PLAYER_INFO_UNSUPPORTED_SUBTITLE:
                if (SHOW_LOGS) Logger.inf(TAG, "onInfo, PLAYER_INFO_UNSUPPORTED_SUBTITLE");
                break;
            case Player.PLAYER_INFO_SUBTITLE_TIMED_OUT:
                if (SHOW_LOGS) Logger.inf(TAG, "onInfo, PLAYER_INFO_SUBTITLE_TIMED_OUT");
                break;
        }
    }

    /**
     * Listener trigger 'onVideoPreparedMainThread' and `onVideoCompletionMainThread` events
     */
    public void setMainThreadMediaPlayerListener(MainThreadMediaPlayerListener listener) {
        mListener = listener;
    }

    public void setVideoStateListener(VideoStateListener listener) {
        mVideoStateListener = listener;
    }

    /**
     * Play or resume video. Video will be played as soon as view is available and media player is
     * prepared.
     * <p/>
     * If video is stopped or ended and play() method was called, video will start over.
     */
    public void start() {
        if (SHOW_LOGS) Logger.v(TAG, ">> start");

        synchronized (mState) {
            if (SHOW_LOGS) Logger.v(TAG, "start, mState " + mState);
            if (mListener != null) {
                Logger.v("MediaPlayerWrapper", "mMainThreadHandler");
                mMainThreadHandler.postTask(mOnVideoPreparedMessage);
            }
            Logger.v(TAG, "mState.get = " + mState.get().toString());
            switch (mState.get()) {
                case IDLE:
                case INITIALIZED:
                case PREPARING:
                case STARTED:
                    throw new IllegalStateException("start, called from illegal state " + mState);

                case STOPPED:
                case PLAYBACK_COMPLETED:
                case PREPARED:
                case PAUSED:
                    if (SHOW_LOGS) Logger.v(TAG, "start, video is " + mState + ", starting playback.");
                    Logger.v(TAG, "mMediaPlayer.prepare() = " + mMediaPlayer.prepare());
                    Logger.v(TAG, "mMediaPlayer.play() = " + mMediaPlayer.play());
                    startPositionUpdateNotifier();
                    mState.set(State.STARTED);
                    break;
                case ERROR:
                case END:
                    throw new IllegalStateException("start, called from illegal state " + mState);
            }
        }
        if (SHOW_LOGS) Logger.v(TAG, "<< start");
    }

    /**
     * Pause video. If video is already paused, stopped or ended nothing will happen.
     */
    public void pause() {
        if (SHOW_LOGS) Logger.v(TAG, ">> pause");

        synchronized (mState) {
            if (SHOW_LOGS)
                Logger.v(TAG, "pause, mState " + mState);

            switch (mState.get()) {
                case IDLE:
                case INITIALIZED:
                case PAUSED:
                case PLAYBACK_COMPLETED:
                case ERROR:
                case PREPARING:
                case STOPPED:
                case PREPARED:
                case END:
                    throw new IllegalStateException("pause, called from illegal state " + mState);

                case STARTED:
                    mMediaPlayer.pause();
                    mState.set(State.PAUSED);
                    break;
            }
        }
        if (SHOW_LOGS) Logger.v(TAG, "<< pause");
    }

    private final Runnable mOnVideoStopMessage = new Runnable() {
        @Override
        public void run() {
            if (SHOW_LOGS) Logger.v(TAG, ">> run, onVideoStoppedMainThread");
            mListener.onVideoStoppedMainThread();
            if (SHOW_LOGS) Logger.v(TAG, "<< run, onVideoStoppedMainThread");
        }
    };

    public void stop() {
        if (SHOW_LOGS) Logger.v(TAG, ">> stop");

        synchronized (mState) {
            if (SHOW_LOGS) Logger.v(TAG, "stop, mState " + mState);

            switch (mState.get()) {

                case STARTED:
                case PAUSED:
                    stopPositionUpdateNotifier();
                    // should stop only if paused or started
                    // FALL-THROUGH
                case PLAYBACK_COMPLETED:
                case PREPARED:
                case PREPARING:

                    if (SHOW_LOGS) Logger.v(TAG, ">> stop");

                    mMediaPlayer.stop();

                    if (SHOW_LOGS) Logger.v(TAG, "<< stop");

                    mState.set(State.STOPPED);

                    if (mListener != null) {
                        mMainThreadHandler.postTask(mOnVideoStopMessage);
                    }
                    break;
                case STOPPED:
                    throw new IllegalStateException("stop, already stopped");

                case IDLE:
                case INITIALIZED:
                case END:
                case ERROR:
                    throw new IllegalStateException("cannot stop. Player in mState " + mState);
            }
        }
        if (SHOW_LOGS) Logger.v(TAG, "<< stop");
    }

    public void reset() {
        if (SHOW_LOGS) Logger.v(TAG, ">> reset , mState " + mState);

        synchronized (mState) {
            switch (mState.get()) {
                case IDLE:
                case INITIALIZED:
                case PREPARED:
                case STARTED:
                case PAUSED:
                case STOPPED:
                case PLAYBACK_COMPLETED:
                case ERROR:
                    mMediaPlayer.reset();
                    mState.set(State.IDLE);
                    break;
                case PREPARING:
                case END:
                    throw new IllegalStateException("cannot call reset from state " + mState.get());
            }
        }
        if (SHOW_LOGS) Logger.v(TAG, "<< reset , mState " + mState);
    }

    public void release() {
        if (SHOW_LOGS) Logger.v(TAG, ">> release, mState " + mState);
        synchronized (mState) {
            mMediaPlayer.release();
            mState.set(State.END);
        }
        if (SHOW_LOGS) Logger.v(TAG, "<< release, mState " + mState);
    }

    public void clearAll() {
        if (SHOW_LOGS) Logger.v(TAG, ">> clearAll, mState " + mState);
        synchronized (mState) {
            mMediaPlayer.setPlayerCallback(null);
        }
        if (SHOW_LOGS) Logger.v(TAG, "<< clearAll, mState " + mState);
    }

    public void setLooping(boolean looping) {
        if (SHOW_LOGS) Logger.v(TAG, "setLooping " + looping);
        mMediaPlayer.enableSingleLooping(looping);
    }

    public void setSurfaceTexture(TextureHolder surfaceTexture) {
        if (SHOW_LOGS) Logger.v(TAG, ">> setSurfaceTexture " + surfaceTexture);
        if (SHOW_LOGS) Logger.v(TAG, "setSurfaceTexture mSurface " + mSurface);


        if (surfaceTexture != null) {
            mSurface = new Surface();
            if (mSurface.bindToTextureHolder(surfaceTexture)) ;
            mMediaPlayer.setVideoSurface(mSurface); // TODO fix illegal state exception
        } else {
            mMediaPlayer.setVideoSurface(null);
        }
        if (SHOW_LOGS) Logger.v(TAG, "<< setmSurface " + mSurface);

    }


    public void setVolume(float leftVolume, float rightVolume) {
        mMediaPlayer.setVolume(leftVolume);
    }

    public int getVideoWidth() {
        return mMediaPlayer.getVideoWidth();
    }

    public int getVideoHeight() {
        return mMediaPlayer.getVideoHeight();
    }

    public int getCurrentPosition() {
        return mMediaPlayer.getCurrentTime();
    }

    public boolean isPlaying() {
        return mMediaPlayer.isNowPlaying();
    }

    public boolean isReadyForPlayback() {
        boolean isReadyForPlayback = false;
        synchronized (mState) {
            if (SHOW_LOGS) Logger.v(TAG, "isReadyForPlayback, mState " + mState);
            State state = mState.get();

            switch (state) {
                case IDLE:
                case INITIALIZED:
                case ERROR:
                case PREPARING:
                case STOPPED:
                case END:
                    isReadyForPlayback = false;
                    break;
                case PREPARED:
                case STARTED:
                case PAUSED:
                case PLAYBACK_COMPLETED:
                    isReadyForPlayback = true;
                    break;
            }

        }
        return isReadyForPlayback;
    }

    public int getDuration() {
        int duration = 0;
        synchronized (mState) {
            switch (mState.get()) {
                case END:
                case IDLE:
                case INITIALIZED:
                case PREPARING:
                case ERROR:
                    duration = 0;

                    break;
                case PREPARED:
                case STARTED:
                case PAUSED:
                case STOPPED:
                case PLAYBACK_COMPLETED:
                    duration = mMediaPlayer.getDuration();
            }
        }
        return duration;
    }

    public void seekToPercent(int percent) {
        synchronized (mState) {
            State state = mState.get();

            if (SHOW_LOGS) Logger.v(TAG, "seekToPercent, percent " + percent + ", mState " + state);

            switch (state) {
                case IDLE:
                case INITIALIZED:
                case ERROR:
                case PREPARING:
                case END:
                case STOPPED:
                    if (SHOW_LOGS) Logger.w(TAG, "seekToPercent, illegal state");
                    break;

                case PREPARED:
                case STARTED:
                case PAUSED:
                case PLAYBACK_COMPLETED:
                    int positionMillis = (int) ((float) percent / 100f * getDuration());
                    mMediaPlayer.rewindTo(positionMillis);
                    notifyPositionUpdated();
                    break;
            }
        }
    }

    private final Runnable mNotifyPositionUpdateRunnable = new Runnable() {
        @Override
        public void run() {
            notifyPositionUpdated();
        }
    };

    private void startPositionUpdateNotifier() {
        if (SHOW_LOGS)
            Logger.v(TAG, "startPositionUpdateNotifier, mPositionUpdateNotifier " + mPositionUpdateNotifier);
        mFuture = mPositionUpdateNotifier.scheduleAtFixedRate(
                mNotifyPositionUpdateRunnable,
                0,
                POSITION_UPDATE_NOTIFYING_PERIOD,
                TimeUnit.MILLISECONDS);
    }

    private void stopPositionUpdateNotifier() {
        if (SHOW_LOGS)
            Logger.v(TAG, "stopPositionUpdateNotifier, mPositionUpdateNotifier " + mPositionUpdateNotifier);

        mFuture.cancel(true);
        mFuture = null;
    }

    private void notifyPositionUpdated() {
        synchronized (mState) { //todo: remove
//            if (SHOW_LOGS) Logger.v(TAG, "notifyPositionUpdated, mVideoStateListener " + mVideoStateListener);

            if (mVideoStateListener != null && mState.get() == State.STARTED) {
                mVideoStateListener.onVideoPlayTimeChanged(mMediaPlayer.getCurrentTime());
            }
        }
    }

    public State getCurrentState() {
        synchronized (mState) {
            return mState.get();
        }
    }

    public static int positionToPercent(int progressMillis, int durationMillis) {
        float percentPrecise = (float) progressMillis / (float) durationMillis * 100f;
        return Math.round(percentPrecise);
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "@" + hashCode();
    }

    public interface MainThreadMediaPlayerListener {
        void onVideoSizeChangedMainThread(int width, int height);

        void onVideoPreparedMainThread();

        void onVideoCompletionMainThread();

        void onErrorMainThread(int what, int extra);

        void onBufferingUpdateMainThread(int percent);

        void onVideoStoppedMainThread();
    }

    public interface VideoStateListener {
        void onVideoPlayTimeChanged(int positionInMilliseconds);
    }

    private boolean inUiThread() {
        return Thread.currentThread().getId() == 1;
    }
}
